using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

namespace Middleware
{
    public static class Marshaller
    {
        private const byte INTEGER = 0;
        private const byte LONG = 1;
        private const byte SHORT = 2;
        private const byte CHAR = 3;
        private const byte STRING = 4;
        private const byte BYTE = 5;
        private const byte INTEGER_ARRAY = 6;
        private const byte OBJECT = 7;

        #region GetBytes

        public static byte[] GetBytes(MessageRequest message) {
            List<byte> bytes = new List<byte>();
            bytes.AddRange(GetBytes(MessageBase.TYPE_REQUEST));
            bytes.AddRange(GetBytes(message.ObjectID));
            bytes.AddRange(GetBytes(message.HasReturnValue));
            bytes.AddRange(GetBytes((byte)message.MethodName.Length));
            bytes.AddRange(GetBytes(message.MethodName));
            bytes.AddRange(GetBytes((byte)message.Parameters.Length));
            foreach (object parameter in message.Parameters) {
                AddObjectToList(bytes, parameter);
            }
            return bytes.ToArray();
        }

        public static byte[] GetBytes(MessageReply message) {
            List<byte> bytes = new List<byte>();
            bytes.AddRange(GetBytes(MessageBase.TYPE_REPLY));
            bytes.AddRange(GetBytes(message.IsError));
            if (message.IsError) {
                bytes.AddRange(GetBytes((byte)message.ErrorMessage.Length));
                bytes.AddRange(GetBytes(message.ErrorMessage));
            } else {
                AddObjectToList(bytes, message.ReturnValue);
            }
            return bytes.ToArray();
        }

        public static byte[] GetBytes(int value) {
            return new[] {
                (byte)( value        & 0xFF),
                (byte)((value >> 8)  & 0xFF),
                (byte)((value >> 16) & 0xFF),
                (byte)((value >> 24) & 0xFF)
            };
        }

        public static byte[] GetBytes(int[] value) {
            List<byte> bytes = new List<byte>();
            foreach (int i in value) {
                bytes.AddRange(GetBytes(i));
            }
            return bytes.ToArray();
        }

        public static byte[] GetBytes(short value) {
            return new[] {
                (byte)( value       & 0xFF),
                (byte)((value >> 8) & 0xFF)
            };
        }

        public static byte[] GetBytes(char value) {
            return new[] {
                (byte)( value       & 0xFF),
                (byte)((value >> 8) & 0xFF)
            };
        }

        public static byte[] GetBytes(string value) {
            byte[] bytes = new byte[value.Length * 2];
            for (int i = 0; i < value.Length; i++) {
                byte[] charBytes = GetBytes(value[i]);
                int index = i * 2;
                bytes[index] = charBytes[0];
                bytes[index + 1] = charBytes[1];
            }
            return bytes;
        }

        public static byte[] GetBytes(byte value) {
            return new[] { value };
        }

        public static byte[] GetBytes(bool value) {
            return value ? new[] { byte.MaxValue } : new byte[] { byte.MinValue };
        }

        public static byte[] GetBytes(long value) {
            return new[] {
                (byte)( value        & 0xFF),
                (byte)((value >> 8)  & 0xFF),
                (byte)((value >> 16) & 0xFF),
                (byte)((value >> 24) & 0xFF),
                (byte)((value >> 32) & 0xFF),
                (byte)((value >> 40) & 0xFF),
                (byte)((value >> 48) & 0xFF),
                (byte)((value >> 56) & 0xFF)
            };
        }

        #endregion // GetBytes

        #region Get message

        public static MessageBase GetMessage(byte[] bytes) {
            byte type = GetByte(bytes.Take(1).ToArray());
            if (type == MessageBase.TYPE_REQUEST) {
                return GetMessageRequest(bytes.Skip(1).ToArray());
            }
            return GetMessageReply(bytes.Skip(1).ToArray());
        }

        public static MessageReply GetMessageReply(byte[] bytes) {
            MessageReply message = new MessageReply();
            int currentPosition = 0;
            message.IsError = GetBoolean(bytes.Take(1).ToArray());
            currentPosition++;
            if (message.IsError) {
                currentPosition++;
                message.ErrorMessage = GetString(bytes.Skip(currentPosition).ToArray());
            } else {
                if (currentPosition < bytes.Length) {
                    message.ReturnValue = GetObjectFromList(bytes, currentPosition, out currentPosition);
                }
            }
            return message;
        }

        public static MessageRequest GetMessageRequest(byte[] bytes) {

            MessageRequest message = new MessageRequest();
            message.ObjectID = GetInt(bytes.Take(4).ToArray());
            message.HasReturnValue = GetBoolean(bytes.Skip(4).Take(1).ToArray());

            byte methodLength = bytes.Skip(5).Take(1).First();
            message.MethodName = GetString(bytes.Skip(6).Take(methodLength * 2).ToArray());
            int currentPosition = 6 + (methodLength * 2);

            byte numberParameters = GetByte(bytes.Skip(currentPosition).Take(1).ToArray());
            currentPosition += 1;
            List<object> parameters = new List<object>();
            for (int i = 0; i < numberParameters; i++) {
                parameters.Add(GetObjectFromList(bytes, currentPosition, out currentPosition));
            }
            message.Parameters = parameters.ToArray();

            return message;
        }

        #endregion

        #region From bytes to any type

        public static short GetShort(byte[] bytes) {
            return (short)
                ((bytes[0] & 0xFF) +
                ((bytes[1] & 0xFF) << 8));
        }

        public static int GetInt(byte[] bytes) {
            return ((bytes[0] & 0xFF) +
                   ((bytes[1] & 0xFF) << 8) +
                   ((bytes[2] & 0xFF) << 16) +
                   ((bytes[3] & 0xFF) << 24));
        }

        public static long GetLong(byte[] bytes) {
            return
                ((bytes[0] & 0xFF) +
                ((bytes[1] & 0xFF) << 8) +
                ((bytes[2] & 0xFF) << 16) +
                ((bytes[3] & 0xFF) << 24) +
                ((bytes[4] & 0xFF) << 32) +
                ((bytes[5] & 0xFF) << 40) +
                ((bytes[6] & 0xFF) << 48) +
                ((bytes[7] & 0xFF) << 56));
        }

        public static char GetChar(byte[] bytes) {
            return (char)
                ((bytes[0] & 0xFF) +
                ((bytes[1] & 0xFF) << 8));
        }

        public static int[] GetIntArray(byte[] bytes) {
            List<int> values = new List<int>();
            for (int i = 0; i < bytes.Length; i += 4) {
                values.Add(GetInt(new[] { bytes[i], bytes[i + 1], bytes[i + 2], bytes[i + 3] }));
            }
            return values.ToArray();
        }

        public static bool GetBoolean(byte[] bytes) {
            return bytes[0] == byte.MaxValue;
        }

        public static string GetString(byte[] bytes) {
            StringBuilder builder = new StringBuilder();
            for (int i = 0; i < bytes.Length; i += 2) {
                builder.Append(GetChar(new byte[] { bytes[i], bytes[i + 1] }));
            }
            return builder.ToString();
        }

        public static byte GetByte(byte[] bytes) {
            return bytes[0];
        }

        public static byte[] GetBytes(object obj) {
            using (MemoryStream ms = new MemoryStream()) {
                BinaryFormatter bf = new BinaryFormatter();
                bf.Serialize(ms, obj);
                return ms.ToArray();
            }
        }

        public static object GetObject(byte[] bytes) {
            using (MemoryStream ms = new MemoryStream(bytes)) {
                BinaryFormatter bf = new BinaryFormatter();
                ms.Position = 0;
                return bf.Deserialize(ms);
            }
        }

        #endregion //From bytes to any type

        #region Helper methods

        private static void AddObjectToList(List<byte> list, object value) {
            if (value is int) {
                list.AddRange(GetBytes(INTEGER));
                list.AddRange(GetBytes((int)value));
                return;
            } else if (value is long) {
                list.AddRange(GetBytes(LONG));
                list.AddRange(GetBytes((long)value));
                return;
            } else if (value is short) {
                list.AddRange(GetBytes(SHORT));
                list.AddRange(GetBytes((short)value));
                return;
            } else if (value is char) {
                list.AddRange(GetBytes(CHAR));
                list.AddRange(GetBytes((char)value));
                return;
            } else if (value is byte) {
                list.AddRange(GetBytes(BYTE));
                list.AddRange(GetBytes((byte)value));
                return;
            } else if (value is string) {
                list.AddRange(GetBytes(STRING));
                list.AddRange(GetBytes((byte)((string)value).Length));
                list.AddRange(GetBytes((string)value));
                return;
            } else if (value is int[]) {
                list.AddRange(GetBytes(INTEGER_ARRAY));
                list.AddRange(GetBytes((byte)((int[])value).Length));
                list.AddRange(GetBytes((int[])value));
                return;
            } else if (value != null) {
                byte[] objectBytes = GetBytes(value);
                list.AddRange(GetBytes(OBJECT));
                list.AddRange(GetBytes(objectBytes.Length));
                list.AddRange(objectBytes);
                return;
            }
        }

        private static object GetObjectFromList(byte[] bytes, int currentPosition, out int outCurrentPosition) {
            byte parameterType = bytes.Skip(currentPosition).Take(1).First();
            currentPosition++;
            object value = null;
            switch (parameterType) {
                case INTEGER: {
                        value = GetInt(bytes.Skip(currentPosition).Take(4).ToArray());
                        currentPosition += 4;
                        break;
                    }
                case LONG: {
                        value = GetLong(bytes.Skip(currentPosition).Take(8).ToArray());
                        currentPosition += 8;
                        break;
                    }
                case SHORT: {
                        value = GetShort(bytes.Skip(currentPosition).Take(2).ToArray());
                        currentPosition += 2;
                        break;
                    }
                case CHAR: {
                        value = GetShort(bytes.Skip(currentPosition).Take(2).ToArray());
                        currentPosition += 2;
                        break;
                    }
                case BYTE: {
                        value = bytes.Skip(currentPosition).Take(1).First();
                        currentPosition += 1;
                        break;
                    }
                case STRING: {
                        byte stringLength = bytes.Skip(currentPosition).Take(1).First();
                        currentPosition++;
                        value = GetString(bytes.Skip(currentPosition).Take(stringLength * 2).ToArray());
                        currentPosition += (stringLength * 2);
                        break;
                    }
                case INTEGER_ARRAY: {
                        byte arrayLength = bytes.Skip(currentPosition).Take(1).First();
                        currentPosition++;
                        value = GetIntArray(bytes.Skip(currentPosition).Take(arrayLength * 4).ToArray());
                        currentPosition += (arrayLength * 4);
                        break;
                    }
                case OBJECT: {
                        int objectLength = GetInt(bytes.Skip(currentPosition).Take(4).ToArray());
                        currentPosition += 4;
                        value = GetObject(bytes.Skip(currentPosition).Take(objectLength).ToArray());
                        currentPosition += objectLength;
                        break;
                    }
            }
            outCurrentPosition = currentPosition;
            return value;
        }

        #endregion // Helper methods
    }
}
